#!/usr/bin/env python3

# _common.py - common functions
#
# Copyright (c) 2019-2023 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import abc
import enum
import importlib
import functools
from ._util import Util


class SystemContext:

    DATA_DIR = "/var/lib/barkspider"

    DATA_DIR_METADATA = (0, 0, 0o40755)

    def get_app_names(self, installed_only=False):
        ret = [name for name, mod in _get_modules().items() if not installed_only or mod.is_installed()]
        ret.sort()
        assert len(set(ret)) == len(ret)
        return ret

    def get_app_info(self, app_name):
        appDataDir = os.path.join(self.DATA_DIR, app_name)
        stat, msg, last_update_time = _get_modules()[app_name].system_get_info(appDataDir)

        assert isinstance(stat, AppStat)
        if stat == AppStat.NOT_INSTALLED:
            assert msg is None and last_update_time is None
        elif stat == AppStat.NOT_MANAGEABLE or stat == AppStat.NOT_ROBUSTIFIED:
            assert msg is not None and last_update_time is None
        else:
            assert msg is not None

        return (stat, msg, last_update_time)

    def robustify_app_config(self, app_names):
        assert len(app_names) > 0

        _ensure_datadir(self.DATA_DIR)

        # FIXME: do network testing first

        for app_name in app_names:
            appDataDir = os.path.join(self.DATA_DIR, app_name)
            _ensure_datadir(appDataDir)
            _get_modules()[app_name].system_robustify_config(appDataDir)

    def refresh_app_config(self):
        _ensure_datadir(self.DATA_DIR)

        appList = []
        for app_name in self.get_app_names():
            appDataDir = os.path.join(self.DATA_DIR, app_name)
            stat, msg, last_update_time = _get_modules()[app_name].system_get_info(appDataDir)
            if stat == AppStat.NOT_INSTALLED:
                # clean redundant data directories
                if os.path.exists(appDataDir):
                    Util.forceDelete(appDataDir)
            elif stat in [AppStat.PARTIALLY_ROBUSTIFIED, AppStat.FULLY_ROBUSTIFIED]:
                # record for the next operation
                appList.append(app_name)

        if len(appList) != 0:
            # FIXME: do network testing first

            for app_name in appList:
                appDataDir = os.path.join(self.DATA_DIR, app_name)
                _ensure_datadir(appDataDir)
                _get_modules()[app_name].system_robustify_config(appDataDir)


class UserContext:

    def __init__(self, username):
        self._username = username

    @property
    def username(self):
        return self._username

    def get_app_names(self, installed_only=False):
        return [name for name, mod in _get_modules().items()]

    def get_app_info(self, app_name):
        state, msg = _get_modules()[app_name].user_get_info(self._username)
        assert isinstance(state, AppStat)
        return (state, msg)

    def robustify_app_config(self, app_names):
        for app_name in app_names:
            return _get_modules()[app_name].user_robustify_config(self._username)


class AppStat(enum.Enum):

    NOT_INSTALLED = enum.auto()             # application is not installed
    NOT_MANAGEABLE = enum.auto()            # application is installed, but unable to determine if it has been robustified or to robustify it
    NOT_ROBUSTIFIED = enum.auto()           # application is good, but has not been robustified
    PARTIALLY_ROBUSTIFIED = enum.auto()     # application is good and has been robustified to the best of barkspider's ability, but flaws still exist
    FULLY_ROBUSTIFIED = enum.auto()         # application is good and is fully robustified

    def __str__(self):
        return _appstat_display_strings[self]


class AppRuntime:

    @property
    def extra_environment_variables(self):
        return []

    @property
    def extra_parameters(self):
        return []


class AppImplBase(abc.ABC):

    @staticmethod
    @abc.abstractmethod
    def get_name():
        pass

    @abc.abstractmethod
    def is_installed(self):
        pass

    @abc.abstractmethod
    def system_get_info(self, app_data_dir):
        pass

    @abc.abstractmethod
    def system_robustify_config(self, app_data_dir):
        pass

    @abc.abstractmethod
    def user_get_info(self, username):
        pass

    @abc.abstractmethod
    def user_robustify_config(self, username):
        pass


_appstat_display_strings = {
    AppStat.NOT_INSTALLED: "Not Installed",
    AppStat.NOT_MANAGEABLE: "Not Manageable",
    AppStat.NOT_ROBUSTIFIED: "Not Robustified",
    AppStat.PARTIALLY_ROBUSTIFIED: "Partially Robustified",
    AppStat.FULLY_ROBUSTIFIED: "Fully Robustified",
}


@functools.cache
def _get_modules():
    selfDir = os.path.dirname(os.path.realpath(__file__))
    ret = {}
    for fn in os.listdir(selfDir):
        if fn.endswith(".py") and not fn.startswith("_"):
            modname = fn[:len(".py")*-1]
            c = getattr(importlib.import_module(".%s" % (modname), package=__package__), "AppImpl")
            ret[c.get_name()] = c()
    return ret


def _ensure_datadir(dataDir):
    if not os.path.exists(dataDir):
        os.mkdir(dataDir)
    else:
        if not os.path.isdir(dataDir):
            raise NotADirectoryError("directory \"%s\" does not exist" % (dataDir))

    st = os.stat(dataDir)
    if st.st_uid != SystemContext.DATA_DIR_METADATA[0] or st.st_gid != SystemContext.DATA_DIR_METADATA[1]:
        os.chown(dataDir, SystemContext.DATA_DIR_METADATA[0], SystemContext.DATA_DIR_METADATA[1])
    if st.st_mode != SystemContext.DATA_DIR_METADATA[2]:
        os.chmod(dataDir, SystemContext.DATA_DIR_METADATA[2])
